{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from __future__ import absolute_import\n",
    "from __future__ import division\n",
    "from __future__ import print_function\n",
    "\n",
    "import collections\n",
    "import math\n",
    "import json\n",
    "import random\n",
    "\n",
    "import numpy as np\n",
    "from six.moves import xrange  # pylint: disable=redefined-builtin\n",
    "import tensorflow as tf\n",
    "\n",
    "from sklearn.manifold import TSNE\n",
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. 读取数据"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "读取数据并转化为单字符列表。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "with open('./quiz-w10-code/QuanSongCi.txt', 'rb') as f:\n",
    "    vocabulary = list(f.read().decode('utf-8'))\n",
    "print('Data size', len(vocabulary))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 构建单字符的索引"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 统计词频，取最常出现的4999个字符，余下的词统一归为UNK，共5000个字符。\n",
    "- 构建由字符映射到索引号的字典（dictionary），以及由索引号映射到字符的字典（reverse dictionary）。\n",
    "- 将原文的字符列表转化为用字符索引表示的数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "vocabulary_size = 5000\n",
    "\n",
    "def build_dataset(words, n_words):\n",
    "    count = [['UNK', -1]]\n",
    "    count.extend(collections.Counter(words).most_common(n_words - 1))    # 统计词频，取最常出现的4999个字符\n",
    "    dictionary = dict()\n",
    "    for word, _ in count:\n",
    "        dictionary[word] = len(dictionary)   # 由字符映射到索引号的字典\n",
    "    data = list()\n",
    "    unk_count = 0\n",
    "    for word in words:\n",
    "        index = dictionary.get(word, 0)\n",
    "        if index == 0:  # dictionary['UNK']\n",
    "            unk_count += 1\n",
    "        data.append(index)     # 将原字符列表转化为索引列表\n",
    "    count[0][1] = unk_count    # UNK的词频\n",
    "    reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))     # 由索引号映射到字符的字典\n",
    "    return data, count, dictionary, reversed_dictionary\n",
    "\n",
    "data, count, dictionary, reverse_dictionary = build_dataset(vocabulary, vocabulary_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 构建训练数据"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "依据 Skip-gram 模型，即根据目标词汇预测上下文的机制，构建训练数据：\n",
    "\n",
    "- 根据所需的上下文长度（skip_window）等距离截取一小段文本，文本长度为 2 * skip_window + 1。\n",
    "- 每个截取文本中间位置的词作为一条输入数据。\n",
    "- 每段截取文本中，除了train_data之外的部分，随机取 num_skips 个词作为 label，与输入数据组成 num_skips 条训练数据。\n",
    "- 将截取文本依次向后移动一个字符重复上述过程，直到组成一个训练 batch。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "data_index = 0\n",
    "\n",
    "def generate_batch(batch_size, num_skips, skip_window):\n",
    "    global data_index\n",
    "    assert batch_size % num_skips == 0\n",
    "    assert num_skips <= 2 * skip_window\n",
    "    batch = np.ndarray(shape=(batch_size), dtype=np.int32)\n",
    "    abels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)\n",
    "    span = 2 * skip_window + 1 \n",
    "    buffer = collections.deque(maxlen=span)    # 用于存放输入词和上下文的队列\n",
    "    if data_index + span > len(data):       # 超过数据长度则从头开始\n",
    "        data_index = 0\n",
    "    buffer.extend(data[data_index:data_index + span])     # 将输入词和指定长度的上下文放入队列\n",
    "    data_index += span\n",
    "    for i in range(batch_size // num_skips):      # 每个batch里不重复的词数\n",
    "        context_words = [w for w in range(span) if w != skip_window]    # 上下文出现的词（去掉了该词本身，即span中间的词）\n",
    "        words_to_use = random.sample(context_words, num_skips)     # 随机选择num_skips个用作label的词\n",
    "        for j, context_word in enumerate(words_to_use):\n",
    "            batch[i * num_skips + j] = buffer[skip_window]     # 输入数据，即span中间的词\n",
    "            labels[i * num_skips + j, 0] = buffer[context_word]    # label，即从上下文中选出的词\n",
    "        if data_index == len(data):\n",
    "            buffer.extend(data[0:span])     # 如果到了文本结尾，则下一条输入数据从文本开头重新开始定义\n",
    "            data_index = span     # 标记结束位置\n",
    "        else:\n",
    "            buffer.append(data[data_index])    # 如果没到文本结尾，则下一条输入数据向后移动一个词\n",
    "            data_index += 1      # 标记结束位置\n",
    "    data_index = (data_index + len(data) - span) % len(data)     # 将结束位置向前移动，避免下一个batch时跳过结尾的词\n",
    "    return batch, labels\n",
    "\n",
    "batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. 构建训练网络"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 初始化 embedding，作为 embeddings 容器，每个值都在-1到1之间随机分布。\n",
    "- 通过 tf.nn.embedding_lookup 查表操作，获得对应索引的 embedding 结果。\n",
    "- 初始化 NCE 权重矩阵，用于计算 nce loss。采用 Xavier 初始化，但在距均值两个标准差处进行了截断。\n",
    "- 利用 NCE 权重矩阵、embedding 后的词向量以及数据的真实 label 计算平均 NCE 损失。\n",
    "- 采用最基本的梯度下降算法优化器 GradientDescentOptimizer进行优化。\n",
    "- 用 embedding 矩阵对验证数据进行 embedding 操作，并计算生成的词向量与其他词向量的余弦相似度。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "batch_size = 128\n",
    "embedding_size = 128  # Dimension of the embedding vector.\n",
    "skip_window = 1       # How many words to consider left and right.\n",
    "num_skips = 2         # How many times to reuse an input to generate a label.\n",
    "num_sampled = 64      # Number of negative examples to sample.\n",
    "\n",
    "valid_size = 16       # Random set of words to evaluate similarity on.\n",
    "valid_window = 100    # Only pick dev samples in the head of the distribution.\n",
    "valid_examples = np.random.choice(valid_window, valid_size, replace=False)\n",
    "\n",
    "graph = tf.Graph()\n",
    "with graph.as_default():\n",
    "    \n",
    "    # 输入数据\n",
    "    train_inputs = tf.placeholder(tf.int32, shape=[batch_size])\n",
    "    train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])\n",
    "    valid_dataset = tf.constant(valid_examples, dtype=tf.int32)\n",
    "    \n",
    "    with tf.device('/cpu:0'):\n",
    "        # 初始化embedding矩阵\n",
    "        embeddings = tf.Variable(\n",
    "            tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))\n",
    "        embed = tf.nn.embedding_lookup(embeddings, train_inputs)\n",
    "\n",
    "        # 初始化NCE权重矩阵\n",
    "        nce_weights = tf.Variable(\n",
    "            tf.truncated_normal([vocabulary_size, embedding_size],\n",
    "                                stddev=1.0 / math.sqrt(embedding_size)))\n",
    "        nce_biases = tf.Variable(tf.zeros([vocabulary_size]))\n",
    "    \n",
    "    # 计算平均损失\n",
    "    loss = tf.reduce_mean(\n",
    "        tf.nn.nce_loss(weights=nce_weights,\n",
    "                       biases=nce_biases,\n",
    "                       labels=train_labels,\n",
    "                       inputs=embed,\n",
    "                       num_sampled=num_sampled,\n",
    "                       num_classes=vocabulary_size))\n",
    "    \n",
    "    optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)\n",
    "    \n",
    "    # 计算 embedding 后的验证数据与其他字符的余弦相似度\n",
    "    norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))\n",
    "    normalized_embeddings = embeddings / norm\n",
    "    valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)\n",
    "    similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)\n",
    "\n",
    "    init = tf.global_variables_initializer()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. 进行训练"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 共训练 400000 个 step。\n",
    "- 每 2000 次输出这两千次的平均损失。\n",
    "- 每 10000 次计算验证词与其他词的相似度，并输出与验证集中的字符最接近的8个字符。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "num_steps = 400000\n",
    "\n",
    "with tf.Session(graph=graph) as session:\n",
    "    init.run()\n",
    "    print('Initialized')\n",
    "    \n",
    "    average_loss = 0\n",
    "    for step in xrange(num_steps):\n",
    "        batch_inputs, batch_labels = generate_batch(\n",
    "            batch_size, num_skips, skip_window)\n",
    "        feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels}\n",
    "\n",
    "        _, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)\n",
    "        average_loss += loss_val\n",
    "        \n",
    "    if step % 2000 == 0:\n",
    "        if step > 0:\n",
    "            average_loss /= 2000    # 每2000次计算一次平均损失\n",
    "        print('Average loss at step ', step, ': ', average_loss)\n",
    "        average_loss = 0\n",
    "\n",
    "    if step % 10000 == 0:\n",
    "        sim = similarity.eval()   # 每10000次计算一次相似度\n",
    "        for i in xrange(valid_size):\n",
    "            valid_word = reverse_dictionary[valid_examples[i]]\n",
    "            top_k = 8  \n",
    "            nearest = (-sim[i, :]).argsort()[1:top_k + 1]    # 取8个最相似的字符（去掉自身）\n",
    "            log_str = 'Nearest to %s:' % valid_word\n",
    "            for k in xrange(top_k):\n",
    "                close_word = reverse_dictionary[nearest[k]]    # 用reverse_dictionary将索引号映射回字符\n",
    "                log_str = '%s %s,' % (log_str, close_word)\n",
    "            print(log_str)\n",
    "        \n",
    "    final_embeddings = normalized_embeddings.eval()\n",
    "\n",
    "# 保存最终生成的embedding\n",
    "np.save('embedding.npy', final_embeddings)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. 结果可视化"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 用 TSNE 将 embedding 后的结果将为两维，便于可视化呈现。\n",
    "- 用 matplotlib 绘制结果，呈现词汇的接近程度。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def plot_with_labels(low_dim_embs, labels, filename):\n",
    "    assert low_dim_embs.shape[0] >= len(labels), 'More labels than embeddings'\n",
    "    plt.figure(figsize=(18, 18)) \n",
    "    for i, label in enumerate(labels):\n",
    "        x, y = low_dim_embs[i, :]\n",
    "        plt.scatter(x, y)\n",
    "        plt.annotate(label,\n",
    "                     xy=(x, y),\n",
    "                     xytext=(5, 2),\n",
    "                     textcoords='offset points',\n",
    "                     ha='right',\n",
    "                     va='bottom')\n",
    "    plt.savefig(filename)\n",
    "\n",
    "mpl.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签\n",
    "mpl.rcParams['axes.unicode_minus'] = False   # 用来正常显示负号\n",
    "\n",
    "tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000, method='exact')    # TSNE降维\n",
    "plot_only = 500  # 只显示前500个字符\n",
    "low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only, :])\n",
    "labels = [reverse_dictionary[i] for i in xrange(plot_only)]   # 用reverse_dictionary将索引号映射回字符\n",
    "plot_with_labels(low_dim_embs, labels, 'tsne.png')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
